《Effective Java》第十章:并发
线程机制是允许同时进行多个活动,并发程序设计比单线程程序设计要困难的多,因为有更多得东西可能会出错,也很难以重现失败。
第66条:同步访问共享的可变数据
关键字synchronized
可以保证在同一时刻,只有一个线程可以执行某一个方法,或者某一个代码块,关于这点,有两方面的意义:
- 当一个对象被一个线程修改的时候,可以阻止另一个线程观察到内部不一致的状态
- 它可以保证进入同步方法或者代码块的每个线程,都可以看到同一个锁保护之前所有的修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22private static boolean stopRequented;
public static void main(String[] args) throws InterruptedException
{
Thread backgroundThread=new Thread(new Runnable(){
@Override
public void run()
{
int i=0;
while(!stopRequented)
i++;
System.out.println("the thread stop now!");
}
});
backgroundThread.start();
Thread.sleep(3000);
stopRequented=true;
}
看上面的代码,你运行的时候并没有按你期望的在3秒之后终止程序,这是因为stopRequented
这个变量并没有同步,不能进行线程共享
虚拟机将那段循环代码可以转为
1 | if(!stopRequented) |
所以解决上述问题的方法就是将其同步:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32private static boolean stopRequented;
//使用同步方法来将变量同步
synchronized private static boolean getStopRequented(){
return stopRequented;
}
synchronized private static void setStopRequented(boolean b)
{
stopRequented=b;
}
public static void main(String[] args) throws InterruptedException
{
Thread backgroundThread=new Thread(new Runnable(){
@Override
public void run()
{
int i=0;
while(!getStopRequented())
i++;
System.out.println("the thread stop now!");
}
});
backgroundThread.start();
Thread.sleep(3000);
setStopRequented(true);
}
另一个更为简单的方法是使用共享 变量关键字volatile
:1
private static volatile boolean stopRequented;
但是volatile
使用时得慎用,因为它只保证了关键字的变量是共享的,但是如果涉及其他操作可能会出现问题,比如++
所以还是建议使用
synchronized
关键字进行同步
简而言之,当多个线程共享数据时,要么直接共享不可变数据,但是如果在共享可变数据的时候,每个读或者写数据的线程必须执行同步,否则会出现非常多意向不到的问题。
第67条:避免过度的同步
同步虽好,但也不能滥用,过度的同步可能会导致性能降低、死锁,甚至不确定行为
先来看下面一个简单的观察者:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50static interface Observable<E>{
void notify(ObservableSet<E> set,E e,String action);
}
static class ObservableSet<E>
{
private final List<E> list=new ArrayList<E>();
private final List<Observable<E>> observers=new ArrayList<Observable<E>>();
/**
* 添加观察者
* @param o
*/
public void addObserver(Observable<E> o)
{
synchronized(observers)
{
observers.add(o);
}
}
/**
* 移除观察者
* @param o
*/
public void removeObserver(Observable<E> o)
{
synchronized(observers)
{
observers.remove(o);
}
}
private void notefiyAll(E e,String action)
{
synchronized(observers)
{
for(Observable<E> o:observers)
o.notify(this, e,action);//通知
}
}
public void add(E e)
{
list.add(e);
notefiyAll(e,"add");
}
}
如果你是这么调用1
2
3
4
5
6
7
8
9
10
11
12
13public static void main(String[] args) throws InterruptedException
{
ObservableSet<Integer> os=new ObservableSet<Integer>();
os.addObserver(new Observable<Integer>(){
public void notify(ObservableSet<Integer> set,Integer e,String action)
{
System.out.println("notify:"+action+" "+e);
}
});
for(int i=0;i<10;i++)
os.add(i);
}
你可以得到正确的结果,但是如果有人使坏。。1
2
3
4
5
6
7
8
9os.addObserver(new Observable<Integer>(){
public void notify(ObservableSet<Integer> set,Integer e,String action)
{
System.out.println("notify:"+action+" "+e);
if(e==3)
set.removeObserver(this);
}
});
你便会收到ConcurrentModificationException
的异常,这是因为它在遍历过程中对集合元素进行了修改,但是synchronized
关键词并无法防止迭代器本身回调到可观察的迭代器中。
其实估计到了这里有人会问:为什么这里不会发生死锁?
因为Java的锁是可以重入的,调用这个程序已经有锁了,因此当该线程试图再次获取该锁时会成功
1 | os.addObserver(new Observable<Integer>(){ |
如果你添加了上述观察者,你就会发现程序就会发生死锁,这是因为后台线程调用了set.removeObserver(o);
,它企图锁定observers
,但是它无法获得该锁,因为主线程已经有锁了,在这期间主线程一直在等待后台线程完成对观察者的删除,然后就砰,发生死锁了。
关于上述问题的解决,可以使用快照:1
2
3
4
5
6
7
8
9
10
11private void notefiyAll(E e,String action)
{
List<Observable<E>> snapshot=null;
synchronized(observers)
{
snapshot=new ArrayList<Observable<E>>(observers);//创建快照 其实就是副本
}
for(Observable<E> o:snapshot)
o.notify(this, e,action);//通知
}
貌似还有可以使用CopyOnWriteArrayList
来代替ArrayList
,他是通过重新拷贝整个底层数组,在这里实现所有的写操作,由于内部永远不会改动,因此也不需要锁定。(Amz ^_^)
简而言之,为了避免死锁和数据破坏,尽量限制同步区域内部的工作量。
第68条:executor和task优于线程
相比
Thread
而言,本条推荐使用Executor
因为:
Thread
在复杂的需求下需要很多代码来进行精细的控制Executor
可以创建工作队列,并进行管理,销毁Executor
可以使用executor.execute(args)
来支持Runable
和callable
的方法Executor
可以使用executor.shutdown
进行优雅的关闭- 对于小程序,可以使用
Executors.newCachedThreadPool
来高效的完成工作 - 对于大负载的服务器,最好使用
Executors.newFixedThreadPool
其实
Executor
感觉起来就是Thread
外围的一个封装,提供了创建,管理,关闭等功能,用起来方便而已,不过相信大公司还都是实现自己的线程池
第69条:并发工具优于wait和notify
Java1.5 以后,应该使用java.util.concurrent
包里面的工具来代替wait
和notify
了,这些工具主要分为三类:
- Executor Framework
- 并发集合(Concurrent Collection)
- 同步器(Synchronizer)
这是因为他们提供更加友好以及便捷的API,还有号称更加快的速度(质疑。。。),还有用这些工具实现并发不容易出错(赞同)
这些工具的简单介绍还是去看书的,它讲的比较散,就不累赘了
直接来总结:直接使用wait
和notify
就像用“并发汇编语言”进行编程一样,而java.util.concurrent
则提供了更高级的语言。如果真的要使用wait
和notify
,那么切记要在while
循环内部去调用wait
,一般情况下,应该优先使用notifyAll
第70条:线程安全性的文档化
月经建议,因为有异常的文档化、继承的文档化、方法的文档化^_^,这也凸显出了文档化得重要性
通过查看文档中是否出现synchronized
修饰符来判断方法是否是线程安全,这个是错误的,因为:
- 在正常的操作中,javadoc并没有在他的输出中包含
synchronized
- 线程安全并不是“要么全有要么全无”,实际上它是有多种安全级别的
所以线程的安全级别是文档中非常重要的说明:
- 不可变的:也就是不需要额外的同步
- 无条件的线程安全:实例是可变的,但是这个类有着足够的内部同步,所以可以被并发使用
- 有条件的线程安全:除了有些方法为进行安全的冰法使用而需要额外的同步之外,这种线程安全级别与无条件的线程安全级别相同
- 非线程安全:你必须自己使用并发手段来让他支持并发
- 线程是对立的:好了,这个类你不管如何做线程都是不安全的-_-(通常是由于没有同步静态数据)
类的线程安全说明通常放在他的文档注释中,但是带有特殊线程安全属性的方法则应该在他们自己的文档注释中说明他们的属性。
简而言之:每个类都应该利用字斟句酌的说明线程安全的注解,清楚地在文档中说明他的线程安全属性。
第71条:慎用延迟初始化
延迟初始化时延迟到需要域的值的时候才将它初始化的这种情况,如果永远不需要这个值,这个域就永远不会被初始化。
但是就像大多数优化一样,对于延迟初始化,最好的建议是“除非绝对必要,否则就不要这么做”。
关于延迟初始化,有大概以下几种方法:
同步访问方法:
1
2
3
4
5
6
7private FieldType field;
synchronized FieldType getFiled(){
if(field==null)
field = computeFiledValue();
return field;
}该方法最简单,也是最清楚
静态域的延迟初始化
1
2
3
4
5
6
7private static class FieldHoler{
static final FieldType field = computeFieldValue();
}
static FieldType getField(){
return FieldHoler.field;
}当getField第一次调用的时候,会读取FieldHoler.field,导致FieldHoler得到初始化,该方法最大的魅力就是不需要同步关键字啊(的确是最大的魅力)
双重检查模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16private volatile FieldType field;//注意要加共享关键字,因为他被初始化之后就不会有锁
FieldType getFieldType getField(){
FieldType result = field;
if(result == null)//第一次检查,看这个域是否被初始化了
{
synchronized(this){//枷锁进行第二次检查
result = field;
if(result == null)//再次判断有没有被初始化
{
field = result = computeFieldValue();
}
}
}
return field;
}该方法的性能很高,可以避免域初始化之后的锁定开销
单重检查模式
1
2
3
4
5
6
7
8
9
10private volatile FieldType field;
FieldType getFieldType getField(){
FieldType result = field;
if(result == null)
{
field = result = computeFieldValue();
}
return field;
}这种方式是要允许这个字段可以被多个线程设置值
其实说了那么多,都说本条的建议是慎用初始化了,所以大多数情况下,这样用是要优于延迟初始化的:1
rivate final FieldType field = computeFieldValue();
简而言之:大多数的域应该正常的进行初始化,而不是延迟初始化(感觉也是,除非有特殊情况,不然正常初始化用的放心啊^_^)
第72条:不要依赖于线程调度器
要编写健壮的、响应良好的、可移植的多应用程序,最好的办法是确保可运行的线程的平均数量不明显多余处理器的数量,这样线程调度器就没有更多的选择,你的程序也不会因为更换了环境而影响了性能。
第73条:避免使用线程组
线程组的初衷是作为一种隔离applet的机制,当然他是出于安全考虑的,但是现在的线程组的安全价值已经差到根本不在java
安全模型的标准工作中提及的地步,并且它还没有很多的功能,而且它们提供许多功能还都是很有缺陷的。